<?php

namespace console\db\channels;

use Yii;
use yii\db\Query;
use yii\helpers\Console;
use yii\helpers\ArrayHelper;
use yii\base\Exception;
use console\db\Migration;

/**
 * 数据表直接复制迁移
 * 
 * @author emhome <emhome@163.com>
 * @since 1.0
 */
abstract class TransferTableMigration extends Migration {

    const PART_MAX = 100000;
    const PART_SIZE = 30000;
    const MERGE_SIZE = 0;

    /**
     * 是否先清理本地数据表
     */
    public $beforeTruncateTargetTable = true;

    /**
     * 源数据库
     */
    public $sourceDb = null;

    /**
     * 源数据库
     */
    public $partIndex = false;

    /**
     * 目标数据库
     */
    public $targetDb = null;

    /**
     * 目标表
     */
    private $_targetTable;

    /**
     * 数据源
     */
    private $_sourceTable;

    /**
     * @inheritdoc
     */
    public function init() {
        parent::init();
        if ($this->db->driverName === 'mysql') {
            $this->tableOptions = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ENGINE=' . $this->tableEngine;
        }
        $table = ArrayHelper::getValue($this->getTransferOptions(), 'table');
        list($this->_targetTable, $this->_sourceTable) = each($table);
    }

    /**
     * 源数据库连接
     * @return \console\db\Connection 源数据库连接
     */
    public function getSourceDb() {
        if ($this->sourceDb !== null) {
            return Yii::$app->{$this->sourceDb};
        }
        return $this->db;
    }

    /**
     * 目标数据库连接
     * @return \console\db\Connection 源数据库连接
     */
    public function getTargetDb() {
        if ($this->targetDb !== null) {
            return Yii::$app->{$this->targetDb};
        }
        return $this->db;
    }

    /**
     * 自定义查询
     * @return \yii\db\Query|null
     */
    public function buildQuery() {
        return null;
    }

    /**
     * 目标表
     * @return string
     */
    public function getTargetTable($merge = false) {
        if ($merge !== false && is_numeric($merge)) {
            return $this->_targetTable . '_' . $merge;
        }
        return $this->_targetTable;
    }

    /**
     * 目标表
     * @return string
     */
    public function getSourceTable() {
        return $this->_sourceTable;
    }

    /**
     * @inheritdoc
     */
    public function safeUp() {
        $options = $this->getTransferOptions();
        $table = ArrayHelper::getValue($options, 'table');
        list($targetTable, $sourceTable) = each($table);
        if (is_numeric($targetTable) && is_string($sourceTable)) {
            $targetTable = $sourceTable;
        }
        $fields = ArrayHelper::getValue($options, 'columns');

        $columns = [];
        while (list($key, $vls) = each($fields)) {
            if (is_string($key)) {
                $columns[$key] = $vls;
            } elseif (is_numeric($key) && is_string($vls)) {
                $columns[$vls] = $vls;
            } else {
                throw new Exception('错误');
            }
        }

        $query = $this->buildQuery();
        if (!($query && $query instanceof Query)) {
            $query = (new Query)->from('{{%' . $sourceTable . '}}');
        }

        $db = $this->getSourceDb();
        $total = $query->count('*', $db);
        if (!$total) {
            Console::output("No data transferred.");
            return;
        }

        if ($this->beforeTruncateTargetTable) {
            $this->truncateTable('{{%' . $targetTable . '}}');
        }
        $callable = [$this, 'transferTable'];
        if ($total > static::PART_MAX) {
            $pagesize = static::PART_SIZE;
            $pages = ceil($total / $pagesize);
            $query->limit($pagesize);

            Console::startProgress(1, $pages, 'Updating objects for parts: ', true);

            if ($pages > 50) {
                for ($i = 0; $i < $pages; $i++) {
                    if (self::MERGE_SIZE) {
                        $this->partIndex = floor(($i * $pagesize) / static::MERGE_SIZE);
                    }
                    echo "\n";
                    echo $pagesize;
                    echo "\n";
                    $query->offset($i * $pagesize);
                    $query->select('id');

                    $subQuery = (new Query())->from(['sub' => $query])
                    ->innerJoin(['m' => $query->from[0]], 'm.id=sub.id')
                    ->select('m.*');

                    $this->handleProgress($subQuery, $callable, $columns, $db);
                    Console::updateProgress($i + 1, $pages);
                }
            } else {
                for ($i = 0; $i < $pages; $i++) {
                    $query->offset($i * $pagesize);
                    $this->handleProgress($query, $callable, $columns, $db);
                    Console::updateProgress($i + 1, $pages);
                }
            }

            Console::endProgress("big data update done.");
        } else {
            $this->handleProgress($query, $callable, $columns, $db);
        }
        $this->afterTransfer($targetTable);
    }

    /**
     * @inheritdoc
     */
    public function safeDown() {
        $targetTable = $this->getTargetTable();
        $tableName = '{{%' . $targetTable . '}}';
        if ($this->isExistsTable($tableName)) {
            $this->truncateTable($tableName);
        }
    }

    /**
     * 表对表之间转移
     * 
     * For example
     * array structure
     * 
     * ```php
     * return [
     *     'table' => [
     *          'targetTableName' => 'sourceTableName'
     *      ],
     *     'columns' => [
     *          'targetTableColumnName' => 'sourceTableColumnName'
     *      ],
     * ];
     * ```
     * 
     * columns source supported callable function.
     * For example
     * 
     * ```php
     * return [
     *     ```
     *     'columns' => [
     *          'targetTableColumnName' => function($row){
     *              //$row is source table row data
     *              //transfer code
     *          },
     *      ],
     * ];
     * ```
     * 
     * @return array
     */
    abstract public function getTransferOptions();

    /**
     * 处理结果
     * @param Migration $migration
     * @param array $rows
     */
    public static function transferTable($migration, $rows, $fields) {
        $data = [];
        foreach ($rows as $row) {
            if (!$migration->validateTransfer($row)) {
                continue;
            }
            $temp = $migration->convertColumnData($row, $fields);
            if (!$temp) {
                continue;
            }
            $data[] = $temp;
        }
        $targetTable = $migration->getTargetTable($migration->partIndex);
        $migration->db->createCommand()->batchInsert('{{%' . $targetTable . '}}', array_keys($fields), $data)->execute();
    }

    /**
     * @inheritdoc
     */
    public function convertColumnData($row, $fields) {
        $data = [];
        foreach ($fields as $name => $field) {
            if (is_string($field)) {
                $data[] = ArrayHelper::getValue($row, $field);
            } elseif (is_callable($field)) {
                $data[] = call_user_func($field, $row);
            } else {
                throw new Exception("field {$name} datatype invalid.");
            }
        }
        return $data;
    }

}
